@tsdiapi/s3
Version:
A TSDIAPI plugin for seamless AWS S3 integration, enabling file uploads, downloads, and presigned URL generation.
449 lines (414 loc) • 16.6 kB
text/typescript
import { PrismaClient } from "@generated/prisma/index.js";
import { Type } from "@sinclair/typebox";
import { JWTGuard, useSession } from "@tsdiapi/jwt-auth";
import { usePrisma } from "@tsdiapi/prisma";
import { useS3Provider } from "@tsdiapi/s3";
import { AppContext, response200, response400, ResponseErrorSchema, addSchema } from "@tsdiapi/server";
export default function UploadModule({ useRoute }: AppContext): void {
const useS3 = useS3Provider();
const prisma = usePrisma<PrismaClient>();
// Схемы для ответов
const FileUploadResponseSchema = addSchema(Type.Object({
url: Type.String(),
key: Type.String(),
}, { $id: 'S3FileUploadResponseSchema' }));
const FileDeleteResponseSchema = addSchema(Type.Object({
status: Type.Boolean(),
}, { $id: 'S3FileDeleteResponseSchema' }));
// Схема для файла с метаданными (используется в нескольких ответах)
// Схема используется через Type.Ref('S3FileWithMetadataSchema') в FileWithMetadataArraySchema
const FileWithMetadataSchema = addSchema(Type.Object({
url: Type.String(),
key: Type.String(),
bucket: Type.String(),
region: Type.String(),
size: Type.Number(),
mimeType: Type.String(),
extension: Type.String(),
name: Type.String(),
ownerId: Type.Optional(Type.String()),
}, { $id: 'S3FileWithMetadataSchema' }));
const FileWithMetadataArraySchema = addSchema(Type.Array(Type.Ref('S3FileWithMetadataSchema')), { $id: 'S3FileWithMetadataArraySchema' });
// Схемы для тел запросов
const FileUploadBodySchema = addSchema(Type.Object({
file: Type.String({
format: 'binary',
}),
deleteKey: Type.Optional(Type.String()),
}, { $id: 'S3FileUploadBodySchema' }));
const FileDeleteBodySchema = addSchema(Type.Object({
key: Type.String(),
}, { $id: 'S3FileDeleteBodySchema' }));
const FileBulkDeleteBodySchema = addSchema(Type.Object({
keys: Type.Optional(Type.Array(Type.String())),
urls: Type.Optional(Type.Array(Type.String())),
}, { $id: 'S3FileBulkDeleteBodySchema' }));
const FileUploadWithDeleteBodySchema = addSchema(Type.Object({
files: Type.Array(Type.String({
format: 'binary',
})),
deleteKeys: Type.Optional(Type.Array(Type.String())),
deleteUrls: Type.Optional(Type.Array(Type.String())),
}, { $id: 'S3FileUploadWithDeleteBodySchema' }));
const FileUploadSimpleBodySchema = addSchema(Type.Object({
files: Type.Array(Type.String({
format: 'binary',
})),
deleteKeys: Type.Optional(Type.Array(Type.String())),
}, { $id: 'S3FileUploadSimpleBodySchema' }));
useRoute('files')
.code(400, ResponseErrorSchema)
.code(403, ResponseErrorSchema)
.auth('bearer')
.guard(JWTGuard())
.code(200, FileUploadResponseSchema)
.post('/public/direct/file/upload')
.description('Upload file')
.body(FileUploadBodySchema)
.acceptMultipart()
.fileOptions({
maxFileSize: 1024 * 1024 * 100,
maxFiles: 1,
})
.handler(async (req, res) => {
if (!req.tempFiles) {
return response400("No file uploaded");
}
const deleteKey = req.body.deleteKey;
if (deleteKey) {
try {
await useS3.deleteFromS3(deleteKey);
} catch (e) { }
}
const results = await useS3.uploadFiles(req.tempFiles?.map(el => {
return {
buffer: el.buffer,
mimetype: el.mimetype,
originalname: el.filename,
}
}));
return response200({
url: results[0].url,
key: results[0].key,
});
})
.build();
useRoute('files')
.code(400, ResponseErrorSchema)
.code(403, ResponseErrorSchema)
.auth('bearer')
.guard(JWTGuard())
.code(200, FileDeleteResponseSchema)
.delete('/public/direct/file')
.description('Delete file')
.body(FileDeleteBodySchema)
.handler(async (req, res) => {
const deleteKey = req.body.key;
if (deleteKey) {
try {
await useS3.deleteFromS3(deleteKey);
} catch (e) { }
}
return response200({
status: true,
});
})
.build();
useRoute('files')
.code(200, FileDeleteResponseSchema)
.code(400, ResponseErrorSchema)
.code(403, ResponseErrorSchema)
.auth('bearer')
.guard(JWTGuard())
.delete('/upload/public')
.body(FileBulkDeleteBodySchema)
.handler(async (req, res) => {
const session = useSession<{ id?: string, userId?: string, adminId?: string }>(req);
const sessionId = session?.id || session?.adminId || session?.userId;
const isAdmin = session?.adminId;
if (!req.body.keys && !req.body.urls) {
return response400("No keys provided");
}
if (req.body.keys) {
for (const key of req.body.keys) {
try {
await useS3.deleteFromS3(key);
} catch (error) {
}
}
try {
await prisma.file.deleteMany({
where: {
key: {
in: req.body.keys,
},
},
});
} catch (error) {
}
}
if (req.body.urls) {
const files = await prisma.file.findMany({
where: {
url: {
in: req.body.urls,
},
},
});
for (const file of files) {
if (isAdmin || file.ownerId === sessionId || file.ownerId === 'common') {
try {
await useS3.deleteFromS3(file.key);
await prisma.file.delete({
where: {
id: file.id,
},
});
} catch (error) {
}
}
}
}
return response200({
status: true,
});
})
.build()
useRoute('files')
.code(400, ResponseErrorSchema)
.code(403, ResponseErrorSchema)
.auth('bearer')
.guard(JWTGuard())
.code(200, FileWithMetadataArraySchema)
.post('/upload/public/images')
.description('Upload public images')
.body(FileUploadWithDeleteBodySchema)
.acceptMultipart()
.fileOptions({
accept: ['image/*'],
maxFileSize: 1024 * 1024 * 10,
maxFiles: 10,
})
.handler(async (req, res) => {
const session = useSession<{ id?: string, userId?: string, adminId?: string }>(req);
if (!req.tempFiles) {
return response400("No file uploaded");
}
const deleteKeys = req.body.deleteKeys || [];
if (deleteKeys.length > 0) {
try {
for (const key of deleteKeys) {
await useS3.deleteFromS3(key);
}
await prisma.file.deleteMany({
where: {
key: {
in: deleteKeys,
},
},
});
} catch (e) { }
}
const deleteUrls = req.body.deleteUrls || [];
if (deleteUrls.length > 0) {
try {
const files = await prisma.file.findMany({
where: {
url: {
in: deleteUrls,
},
},
});
for (const file of files) {
await useS3.deleteFromS3(file.key);
await prisma.file.delete({
where: {
id: file.id,
},
});
}
} catch (e) {
}
}
const results = await useS3.uploadFiles(req.tempFiles?.map(el => {
return {
buffer: el.buffer,
mimetype: el.mimetype,
originalname: el.filename,
}
}));
await prisma.file.createMany({
data: results.map(el => {
return {
name: el.meta?.name!,
ownerId: session?.id || session?.adminId || session?.userId || null,
url: el.url,
key: el.key,
bucket: el.bucket,
region: el.region,
size: el.meta?.size!,
mimeType: el.meta?.type!,
extension: el.meta?.extension!,
}
})
})
return response200(results.map(el => {
return {
url: el.url,
key: el.key,
bucket: el.bucket,
region: el.region,
size: el.meta?.size!,
mimeType: el.meta?.type!,
extension: el.meta?.extension!,
name: el.meta?.name!,
ownerId: session?.id || session?.adminId || session?.userId || null,
}
}))
})
.build();
useRoute('files')
.code(403, ResponseErrorSchema)
.code(200, FileWithMetadataArraySchema)
.code(400, ResponseErrorSchema)
.auth('bearer')
.guard(JWTGuard())
.post('/upload/public/documents')
.body(FileUploadSimpleBodySchema)
.acceptMultipart()
.fileOptions({
accept: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
maxFileSize: 1024 * 1024 * 50,
maxFiles: 10,
})
.description('Upload public documents')
.handler(async (req, res) => {
const session = useSession<{ id?: string, userId?: string, adminId?: string }>(req);
if (!req.tempFiles) {
return response400("No file uploaded");
}
const deleteKeys = req.body.deleteKeys || [];
if (deleteKeys.length > 0) {
try {
for (const key of deleteKeys) {
await useS3.deleteFromS3(key);
}
await prisma.file.deleteMany({
where: {
key: {
in: deleteKeys,
},
},
});
} catch (e) { }
}
const results = await useS3.uploadFiles(req.tempFiles?.map(el => {
return {
buffer: el.buffer,
mimetype: el.mimetype,
originalname: el.filename,
}
}));
await prisma.file.createMany({
data: results.map(el => {
return {
name: el.meta?.name!,
ownerId: session?.id || session?.adminId || session?.userId || null,
url: el.url,
key: el.key,
bucket: el.bucket,
region: el.region,
size: el.meta?.size!,
mimeType: el.meta?.type!,
extension: el.meta?.extension!,
}
})
})
return response200(results.map(el => {
return {
url: el.url,
key: el.key,
bucket: el.bucket,
region: el.region,
size: el.meta?.size!,
mimeType: el.meta?.type!,
extension: el.meta?.extension!,
name: el.meta?.name!,
ownerId: session?.id || session?.adminId || session?.userId || null,
}
}))
})
.build();
useRoute('files')
.code(403, ResponseErrorSchema)
.code(200, FileWithMetadataArraySchema)
.code(400, ResponseErrorSchema)
.auth('bearer')
.guard(JWTGuard())
.post('/upload/public/media')
.body(FileUploadSimpleBodySchema)
.acceptMultipart()
.fileOptions({
accept: ['video/*', 'audio/*'],
maxFileSize: 1024 * 1024 * 50,
maxFiles: 10,
})
.description('Upload public media')
.handler(async (req, res) => {
const session = useSession<{ id?: string, userId?: string, adminId?: string }>(req);
if (!req.tempFiles) {
return response400("No file uploaded");
}
const deleteKeys = req.body.deleteKeys || [];
if (deleteKeys.length > 0) {
try {
for (const key of deleteKeys) {
await useS3.deleteFromS3(key);
}
await prisma.file.deleteMany({
where: {
key: {
in: deleteKeys,
},
},
});
} catch (e) { }
}
const results = await useS3.uploadFiles(req.tempFiles?.map(el => {
return {
buffer: el.buffer,
mimetype: el.mimetype,
originalname: el.filename,
}
}));
await prisma.file.createMany({
data: results.map(el => {
return {
name: el.meta?.name!,
url: el.url,
key: el.key,
bucket: el.bucket,
region: el.region,
size: el.meta?.size!,
mimeType: el.meta?.type!,
extension: el.meta?.extension!,
ownerId: session?.id || session?.adminId || session?.userId || null,
}
})
})
return response200(results.map(el => {
return {
url: el.url,
key: el.key,
bucket: el.bucket,
region: el.region,
size: el.meta?.size!,
mimeType: el.meta?.type!,
extension: el.meta?.extension!,
name: el.meta?.name!,
ownerId: session?.id || session?.adminId || session?.userId || null,
}
}))
})
.build();
}